package calpi.database;

import java.io.IOException;
import java.io.File;
import java.io.FileNotFoundException;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.NoSuchElementException;

/**
 * Clase responsável pela comunição, inserção e remoção de cadastros. Usa-se um
 * arquivo de entrada e um de saída. O mais comum é o arquivo de entrada ser o
 * mesmo de saída. Usando arquivos diferentes poderá ser útil para a criação de
 * backups
 * @author Sem iNick
 */
public abstract class DataBaseConnector<T> implements Iterable<T>, Iterator<T> {

    /**
     * Usado no tipo de iteração que será feita. Itera o buffer de entrada
     * @see #setIterableBuffer(int)
     */
    public static final int BUFFER_IN = 0;

    /**
     * Usado no tipo de iteração que será feita. Itera o buffer de saída
     * @see #setIterableBuffer(int)
     */
    public static final int BUFFER_OUT = 1;

    //Buffer de entrada dos cadastros
    protected T[] bufferIn;

    //Buffer usada nas modificações
    protected ArrayList<T> bufferOut = new ArrayList<T>();

    //Arquivo de leitura
    private File fileIn;
    
    //Arquivo de saída
    private File fileOut;

    //Usado para a organizar o buffer de saída
    private Comparator<T> compa;

    //Usado na iteração
    private int atual = 0;

    //Buffer que está sendo iterado
    private int bufIte;

    /**
     * Cria um connector para gravação e leitura de dados
     * @param fileIn Arquivo que será usado como arquivo de entrada
     * @param fileOut Arquivo que será usado com arquivo de saída
     * @throws NullPointerException Se fileIn ou fileOut forem nulo
     * @throws IOException Se fileIn ou fileOut não existirem e não for possível
     * criá-los
     */
    public DataBaseConnector(String fileIn, String fileOut)
                throws FileNotFoundException, IOException {

        setFileIn(fileIn);
        setFileOut(fileOut);
    }

    /**
     * Cria um connector para gravação e leitura de dados
     * @param fileIn Arquivo que será usado como arquivo de entrada
     * @param fileOut Arquivo que será usado com arquivo de saída
     * @throws NullPointerException Se fileIn ou fileOut forem nulo
     * @throws FileNotFoundException Se fileIn não existir
     * @throws IOException Se fileOut ou fileIn não existirem e não for possível
     * criá-los
     */
    public DataBaseConnector(File fileIn, File fileOut)
                throws FileNotFoundException, IOException {

        setFileIn(fileIn);
        setFileOut(fileOut);
    }

    /**
     * Cria um connector para gravação e leitura de dados
     * @param file Arquivo que será usado como arquivo de entrada e saída
     * @throws NullPointerException Se file for nulo
     * @throws IOException Se file não existir e não for possível criá-la
     */
    public DataBaseConnector(File file)
                throws FileNotFoundException, IOException {

        setFileIn(file);
        setFileOut(file);
    }

    /**
     * Cria um connector para gravação e leitura de dados
     * @param file Arquivo que será usado como arquivo de entrada e saída
     * @throws NullPointerException Se file for nulo
     * @throws IOException Se file não existir e não for possível criá-la
     */
    public DataBaseConnector(String file)
                throws FileNotFoundException, IOException {

        setFileIn(file);
        setFileOut(file);
    }

    /**
     * Setter para o nome do arquivo usado para leitura
     * @param file Nome do arquivo
     * @throws NullPointerException Se nome do arquivo for nulo
     * @throws IOException Se o arquivo de entrada não existir e não for
     * possível criá-lo
     */
    public final void setFileIn(String file) throws IOException {

        setFileIn(new File(file));
    }

    /**
     * Setter para o nome do arquivo usado para leitura
     * @param file Objeto relacionada ao arquivo de entrada
     * @throws NullPointerException Se nome do arquivo for nulo
     * @throws IOException Se o arquivo de entrada não existir e não for
     * possível criá-lo
     */
    public final void setFileIn(File file) throws IOException {

        if (file == null) {

            throw new NullPointerException("Arquivo de entrada nulo.");
        }
        
        if (!file.exists()) {

            file.createNewFile();
        }

        fileIn = file;
    }

    /**
     * Setter para o arquivo que será na gravação dos dados
     * @param fOut Caminho para o arquivo de saída
     * @throws IOException Caso o arquivo não exista, será tentado criá-lo.
     * @throws NullPointerException Se caminho do arquivo for nulo
     */
    public final void setFileOut(String fOut) throws IOException {

        setFileOut(new File(fOut));
    }

    public final void setFileOut(File fOut) throws IOException {

        if (fOut == null) {

            throw new NullPointerException("Arquivo de saída nulo.");
        }

        //Verifica se o arquivo existe e tenta criá-lo
        if (!fOut.exists()) {

            fOut.createNewFile();
        }

        fileOut = fOut;
    }

    public final File getFileIn() {

        return fileIn;
    }

    public final File getFileOut() {

        return fileOut;
    }

    public T[] getBufferOut() {

        if ((bufferIn == null) || (bufferIn.length == 0)) {

            return null;
        }
        T[] a = (T[]) Array.newInstance(bufferIn[0].getClass(), bufferOut.size());
        return bufferOut.toArray(a);
    }

    @Override
    public Iterator<T> iterator() {

        atual = 0;
        return this;
    }

    @Override
    public boolean hasNext() {

        switch (getIterableBuffer()) {

            case BUFFER_IN:

                if ((bufferIn == null) || (bufferIn.length == 0)) {

                    return false;
                }

                return atual != bufferIn.length;

            case BUFFER_OUT:

                if ((bufferOut == null) || (bufferOut.size() == 0)) {

                    return false;
                }

                return atual != bufferOut.size();
        }

        //Returna falso para caso o tipo que está sendo iterado não possa ser iterado O.o
        return false;
    }

    @Override
    /**
     * Antes de chamar esse método pela primeira vez, é necessário ler os dados
     * para o buffer de entrada
     * @throws NoSuchElementException Se não houver mais elementos
     * @see DataBaseConnector#readFully
     */
    public T next() {

        if (!hasNext()) {

            throw new NoSuchElementException();
        }

        switch (getIterableBuffer()) {

            case BUFFER_IN:

                return bufferIn[atual++];

            case BUFFER_OUT:

                return bufferOut.get(atual++);
        }

        return null;
    }

    /**
     * Esse método sempre lançara um exceção.
     * @throws UnsupportedOperationException Sempre
     */
    @Override
    public void remove() {

        throw new UnsupportedOperationException();
    }

    /**
     * Retorna um elemento do buffer de entrada em uma posição específica
     * @param index Posição do elemento
     * @return Elemento da posição dada
     * @throws ArrayIndexOutOfBoundsException Se index não for uma posição válida
     */
    public final T getElementAtBufferIn(int index) {

        return bufferIn[index];
    }

    /**
     * Pega o elemento do buffer de saída na posição dada
     * @param index Posição que será acessada
     * @return Elemento da posição dada
     * @throws ArrayIndexOutOfBoundsException Se não for passado um index válido
     */
    public final T getElementAtBufferOut(int index) {

        return bufferOut.get(index);
    }

    /**
     * Substitui um elemento do buffer de saída por um novo elemento
     * @param index Posição onde se encotra o elemento a ser substituido
     * @param element Novo elemento da posição
     * @return Elemento que se encotrava na posição
     * @throws ArrayIndexOutOfBoundsException Se inde não for válido
     */
    public final T setElementAtBufferOut(int index, T element) {

        return bufferOut.set(index, element);
    }

    /**
     * Remove um elemento de uma determinda posição
     * @param index Posição de onde o elemento será removido
     * @see DataBaseConnector#removeElementBufferOut(java.lang.Object)
     */
    public final void removeElementBufferOut(int index) {

        bufferOut.remove(index);
    }

    /**
     * Remove um elemento do buffer de saída
     * @param element Elemento que será removido
     * @see DataBaseConnector#removeElementBufferOut(int) 
     */
    public final void removeElementBufferOut(T element)  {

        bufferOut.remove(element);
    }

    /**
     * Adiciona um novo elemento do final do buffer de saída
     * @param element Elemento que será adicionado
     */
    public final void addElementBufferOut(T element) {

        bufferOut.add(element);
    }

    /**
     * Procura a primeira ocorrência de um elemento e pega sua posição
     * @param element Elemento a ser procurado
     * @return Posição do elemento ou -1 se não for encontrado
     */
    public final int getPosElement(T element) {

        return bufferOut.indexOf(element);
    }

    /**
     * Troca um elemento por outro no buffer de saída
     * @param novoElemento Elemento que será colocado
     * @param velhoElemento Elemento que será retirado
     * @return true caso a troca tenha sido feita com sucesso ou false caso contrário
     */
    public final boolean substituiElemento(T novoElemento, T velhoElemento) {

        int pos = getPosElement(velhoElemento);
        if (pos == -1) {

            return false;
        }
        
        bufferOut.set(pos, novoElemento);
        return true;
    }

    /**
     * Adiciona vários elementos ao buffer de saída
     * @param elements Elementos a serem adicionados
     * @throws NullPointerException Se elements for nulo
     */
    public final void addElementsBufferOut(T[] elements) {

        if (elements == null) {

            throw new NullPointerException("Elementos nulo");
        }

        for (T e : elements) {

            addElementBufferOut(e);
        }
    }

    /**
     * Seta o objeto que será usadado na organização do buffer de saída
     * @param comp Objeto usada na comparação
     * @see DataBaseConnector#sortBufferOut()
     */
    public final void setComparator(Comparator<T> comp) {

        compa = comp;
    }

    /**
     * Organiza o buffer de saída usando o Comparator setado para este objeto
     * @throws NullPointerException Se o Comparator do objeto for nulo
     * @see DataBaseConnector#setComparator(java.util.Comparator) 
     */
    public final void sortBufferOut() {

        if (compa == null) {

            throw new NullPointerException("Comparator nulo");
        }

        T[] a = (T[]) bufferOut.toArray();
        Arrays.sort(a, compa);
        bufferOut.clear();
        for (T e : a) {

            bufferOut.add(e);
        }
    }

    /**
     * Especifica sobre qual circunstância o iterator agirá
     * @param buffer Tipo do iterator
     * @throws IllegalArgumentException Se buffer não for um tipo válido que possa
     * ser iterado
     */
    public final void setIterableBuffer(int buffer) {

        switch (buffer) {

            case BUFFER_IN:

                bufIte = BUFFER_IN;
            break;

            case BUFFER_OUT:

                bufIte = BUFFER_OUT;
            break;

            default:

                throw new IllegalArgumentException("Impossível iterar sobre esse tipo");
        }
    }

    /**
     * Especifíca em que circunstância o iterator está agindo
     * @return Tipo do buffer que está sendo iterado
     */
    public int getIterableBuffer() {

        return bufIte;
    }
    /**
     * Lê todos os dados do arquivo de entrada. Esse método deve ler e tratar
     * os dados para a criação dos registros
     * @throws IOException Se ocorrer algum erro de E/S
     */
    public abstract void readFully() throws IOException;

    /**
     * Escreve os dados gravados do buffer de saída para o arquivo de saída.
     * @throws IOException Se ocorrer algum error E/S
     */
    public abstract void writeBuffer() throws IOException;

    /**
     * Fecha a conexão com o banco de dados
     */
    public abstract void closeConnection() throws Throwable;
}
